import javax.swing.Timer;
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.List;
import java.util.*;

public class BeatingHeart extends JPanel implements ActionListener {
    private static final int CANVAS_WIDTH = 640;
    private static final int CANVAS_HEIGHT = 480;
    private static final int CANVAS_CENTER_X = CANVAS_WIDTH / 2;
    private static final int CANVAS_CENTER_Y = CANVAS_HEIGHT / 2;
    private static final double IMAGE_ENLARGE = 11;
    private static final String HEART_COLOR = "#ff0000";
    private static final int TIMER_DELAY = 160; // milliseconds

    private int frame = 0;
    private final Heart heart = new Heart(20);

    public BeatingHeart() {
        Timer timer = new Timer(TIMER_DELAY, this);
        timer.start();
    }

    @Override
    protected void paintComponent(Graphics g) {
        super.paintComponent(g);
        Graphics2D g2d = (Graphics2D) g;
        g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
        g2d.setColor(Color.decode(HEART_COLOR));
        heart.render(g2d, frame);
        frame++;
    }

    @Override
    public void actionPerformed(ActionEvent e) {
        repaint();
    }

    private static void createAndShowGUI() {
        JFrame frame = new JFrame("Beating Heart");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setSize(CANVAS_WIDTH, CANVAS_HEIGHT);
        frame.add(new BeatingHeart());
        frame.setVisible(true);
    }

    public static void main(String[] args) {
        SwingUtilities.invokeLater(BeatingHeart::createAndShowGUI);
    }


    class Heart {
        private final Set<Point> points = new HashSet<>();
        private final Set<Point> edgeDiffusionPoints = new HashSet<>();
        private final Set<Point> centerDiffusionPoints = new HashSet<>();
        private final List<List<Point>> allPoints = new ArrayList<>();
        private final Random random = new Random();

        public Heart(int generateFrame) {
            build(2000);

            for (int frame = 0; frame < generateFrame; frame++) {
                calc(frame);
            }
        }

        private void build(int number) {
            for (int i = 0; i < number; i++) {
                double t = random.nextDouble() * 2 * Math.PI;
                Point p = heartFunction(t);
                points.add(p);
            }

            for (Point p : new HashSet<>(points)) {
                for (int i = 0; i < 3; i++) {
                    Point q = scatterInside(p);
                    edgeDiffusionPoints.add(q);
                }
            }

            List<Point> pointList = new ArrayList<>(points);
            for (int i = 0; i < 4000; i++) {
                Point p = pointList.get(random.nextInt(pointList.size()));
                Point q = scatterInside(p);
                centerDiffusionPoints.add(q);
            }
        }

        private Point heartFunction(double t) {
            double x = 16 * Math.pow(Math.sin(t), 3);
            double y = -(13 * Math.cos(t) - 5 * Math.cos(2 * t) - 2 * Math.cos(3 * t) - Math.cos(4 * t));

            x *= IMAGE_ENLARGE;
            y *= IMAGE_ENLARGE;

            x += CANVAS_CENTER_X;
            y += CANVAS_CENTER_Y;

            return new Point((int) x, (int) y);
        }

        private Point scatterInside(Point p) {
            double beta = 0.15;
            double ratioX = -beta * Math.log(random.nextDouble());
            double ratioY = -beta * Math.log(random.nextDouble());

            double dx = ratioX * (p.x - CANVAS_CENTER_X);
            double dy = ratioY * (p.y - CANVAS_CENTER_Y);

            int x = (int) (p.x - dx);
            int y = (int) (p.y - dy);

            return new Point(x, y);
        }

        private void calc(int generateFrame) {
            double ratio = 10 * curve(generateFrame / 10.0 * Math.PI);

            int haloRadius = (int) (4 + 6 * (1 + curve(generateFrame / 10.0 * Math.PI)));
            int haloNumber = (int) (3000 + 4000 * Math.abs(curve(generateFrame / 10.0 * Math.PI)));

            List<Point> framePoints = new ArrayList<>();

            Set<Point> heartHaloPoint = new HashSet<>();
            for (int i = 0; i < haloNumber; i++) {
                double t = random.nextDouble() * 2 * Math.PI;
                Point p = heartFunction(t);
                p = shrink(p, haloRadius);
                if (!heartHaloPoint.contains(p)) {
                    heartHaloPoint.add(p);
                    p.translate(random.nextInt(28) - 14, random.nextInt(28) - 14);
                    framePoints.add(p);
                }
            }

            for (Point p : points) {
                Point q = calcPosition(p, ratio);
                framePoints.add(q);
            }

            for (Point p : edgeDiffusionPoints) {
                Point q = calcPosition(p, ratio);
                framePoints.add(q);
            }

            for (Point p : centerDiffusionPoints) {
                Point q = calcPosition(p, ratio);
                framePoints.add(q);
            }

            allPoints.add(framePoints);
        }

        private Point shrink(Point p, int ratio) {
            double force = -1 / Math.pow(Math.pow(p.x - CANVAS_CENTER_X, 2) + Math.pow(p.y - CANVAS_CENTER_Y, 2), 0.6);
            double dx = ratio * force * (p.x - CANVAS_CENTER_X);
            double dy = ratio * force * (p.y - CANVAS_CENTER_Y);

            int x = (int) (p.x - dx);
            int y = (int) (p.y - dy);

            return new Point(x, y);
        }

        private Point calcPosition(Point p, double ratio) {
            double force = 1 / Math.pow(Math.pow(p.x - CANVAS_CENTER_X, 2) + Math.pow(p.y - CANVAS_CENTER_Y, 2), 0.52);
            double dx = ratio * force * (p.x - CANVAS_CENTER_X) + random.nextInt(3) - 1;
            double dy = ratio * force * (p.y - CANVAS_CENTER_Y) + random.nextInt(3) - 1;

            int x = (int) (p.x - dx);
            int y = (int) (p.y - dy);

            return new Point(x, y);
        }

        private double curve(double p) {
            return 2 * (2 * Math.sin(4 * p)) / (2 * Math.PI);
        }

        public void render(Graphics2D g2d, int renderFrame) {
            List<Point> framePoints = allPoints.get(renderFrame % allPoints.size());
            for (Point p : framePoints) {
                g2d.fillRect(p.x, p.y, 2, 2);
            }
        }
    }
}